Skip to content

Migrate TUI from cursive to ratatui#3905

Open
iho wants to merge 3 commits into
mimblewimble:stagingfrom
iho:tui-ratatui-migration
Open

Migrate TUI from cursive to ratatui#3905
iho wants to merge 3 commits into
mimblewimble:stagingfrom
iho:tui-ratatui-migration

Conversation

@iho

@iho iho commented Jul 10, 2026

Copy link
Copy Markdown

Summary

  • Replaces the cursive/cursive_table_view TUI (which pulls in pancurses/ncurses) with ratatui + crossterm, which have no external C dependency
  • Fixes the cross-compilation failures, macOS mouse-click crashes, and Windows first-run crashes described in the issue
  • All 5 screens (Basic Status, Peers and Sync, Mining, Logs, Version Info) and their keybindings (j/k/arrows, Tab, Enter, Esc, q, plus w/d to toggle the Mining sub-view) are preserved
  • New: mouse support (click a menu item to switch tabs, scroll wheel moves table selection — safe now that ncurses is gone), PageUp/PageDown/Home/End navigation in tables, and throttled redraws (repaint only on input or new data, 250ms cap) to reduce idle CPU
  • Interactive column sorting (cursive_table_view) is dropped, since ratatui::widgets::Table has no equivalent — tables render in the order ServerStats provides. Can be revisited as a follow-up if wanted.
  • Controller::new/Controller::run()'s public API (used by src/bin/cmd/server.rs) is unchanged
  • Windows note: key events are filtered to KeyEventKind::Press, avoiding the classic doubled-keystroke issue with crossterm on Windows

Test plan

  • cargo build --bin grin — 0 errors
  • cargo test --bin grin tui::status — both update_sync_status unit tests pass unmodified
  • cargo clippy --bin grin — zero findings in src/bin/tui
  • Confirmed via otool -L the built binary has no ncurses/pancurses linkage
  • Manually drove the TUI against a live testnet node in a tmux pty: all 5 tabs render with real peer/mining/sync data; keyboard navigation, mining workers/difficulty toggle, table selection clamping, Home/End/PageUp/PageDown, mouse tab-clicks and wheel scrolling (via SGR escape injection) all verified; error-dialog path renders correctly; q exits cleanly with exit code 0 and the terminal fully restored (raw mode off, alternate screen left, mouse capture disabled)

Fixes #3841

@iho
iho changed the base branch from master to staging July 10, 2026 16:39
Comment thread src/bin/tui/app.rs Outdated
@@ -0,0 +1,152 @@
// Copyright 2024 The Grin Developers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2026?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks

@wiesche89 wiesche89 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rebase (merge conflicts)

iho added 3 commits July 20, 2026 18:28
Replaces the cursive/pancurses-backed TUI with ratatui + crossterm,
which has no external C dependency (no more linking libncurses). This
fixes cross-compilation failures, macOS crashes on mouse clicks, and
Windows crashes on first run reported in mimblewimble#3841.

The cursive retained-mode widget tree is replaced with a single App
struct (src/bin/tui/app.rs) that every screen renders from each frame.
All previous behavior (status fields, peer/mining table columns and
formatting, bottom-anchored log view, keybindings, Controller's public
API) is preserved, with a few additions:

- Mouse support: click a menu item to switch tabs, scroll wheel moves
  table selection (safe now that ncurses is gone)
- PageUp/PageDown/Home/End navigation in tables, with selection
  clamped to the table's row count
- Redraws are throttled: the UI only repaints on input or new data,
  with a 250ms cap to keep time-based fields fresh

Interactive column sorting (cursive_table_view) is dropped since
ratatui's Table has no equivalent; tables render in the order
ServerStats provides.

Fixes mimblewimble#3841
Removes the now-unused cursive/pancurses dependency tree and
resolves the ratatui/crossterm graph against the current staging
lockfile.
@iho
iho force-pushed the tui-ratatui-migration branch from c08dfce to 0f62d93 Compare July 20, 2026 15:32
@iho
iho requested review from ardocrat and wiesche89 July 20, 2026 15:33

@wiesche89 wiesche89 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ratatui looks like the right direction and works well on macOS. I couldn’t test Linux or Windows. I left a few comments that I’d prefer to address in this PR so the migration lands cleanly.

Comment thread src/bin/tui/ui.rs
self.cursive.step();
if self.needs_redraw || self.last_draw.elapsed() >= MAX_REDRAW_INTERVAL {
let app = &mut self.app;
let _ = self.terminal.draw(|f| draw(f, app));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we propagate terminal input and render errors instead of dropping them? A failed poll, read, or draw currently leaves the controller running without a usable UI, and a failed draw is still marked as completed.

Comment thread src/bin/tui/ui.rs
if show_dialog_clone.load(Ordering::Relaxed) {
c.pop_layer();
fn handle_key(&mut self, code: KeyCode) {
if let Some(dialog) = &self.app.dialog {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mouse input is blocked for every dialog, but keyboard input is only blocked for errors. During startup, Tab/j/Enter can still change the view behind the info dialog. Could we make these dialogs modal while still allowing q to shut down?

Comment thread src/bin/tui/logs.rs
for raw_line in text.split('\n') {
let mut current = String::new();
let mut current_len = 0usize;
for word in raw_line.split(' ') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use Paragraph::wrap() and line_count() here instead of maintaining a separate wrapping implementation? That would preserve terminal display width and whitespace while still allowing the log view to stay bottom aligned.

Comment thread src/bin/tui/ui.rs
state.select(None);
return;
}
let current = state.selected().unwrap_or(0) as i64;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TableState starts at None, so the first Down/j/scroll jumps straight to row 1. Could we anchor the first move to row 0 and cover it with a small test?

Comment thread src/bin/tui/constants.rs
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGG
pub const _WELCOME_LOGO: &str = " GGGGG GGGGGGG

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the migration this module only contains the unused _WELCOME_LOGO. Could we remove the file and mod constant, as part of the cleanup?

Comment thread src/bin/tui/ui.rs
pub fn stop(&mut self) {
self.cursive.quit();
self.app.should_quit = true;
let _ = disable_raw_mode();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The terminal restore sequence is now repeated in the panic hook, stop(), and Drop. Could we centralize it and make the cleanup so these paths cannot drift?

Comment thread src/bin/tui/ui.rs
_ => {}
},
match message {
UIMessage::UpdateStatus(update) => self.app.stats = Some(update),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the UI owns App and status updates happen on the same controller thread, could we update app.stats directly and remove the single-message UI channel? It looks like leftover Cursive plumbing.

Comment thread src/bin/tui/status.rs
lines.push(line("Stem Pool Size:", "0 (0)"));
}
lines.push(Line::from(SEPARATOR));
// These three lines are reserved for mining config/status/network info,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These fields were never populated in the old view. Could we drop the reserved blank rows instead of carrying the unused placeholders into the new renderer?

Comment thread src/bin/tui/ui.rs

/// Redraw at least this often even without input or new data, so
/// time-based fields (peer "last seen" ages, etc.) stay fresh.
const MAX_REDRAW_INTERVAL: Duration = Duration::from_millis(250);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The time based fields only change once per second, while 250 ms still rebuilds the full UI four times per second when idle. Could this use a one-second refresh, or is there a measurement supporting the shorter interval?

Comment thread src/bin/tui/logs.rs
let height = area.height as usize;

LogBufferView { buffer }
// Walk entries newest-first, wrapping each until we have enough rows to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s quite a bit of commentary walking through this rendering step by step. Could we simplify it by using Ratatui’s wrapping and line counting support, then keep only the non obvious bottom alignment note?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate tui from cursive to ratatui

3 participants